/*
XPTracker - Alternative Agile Tracking Tool
Copyright (C) 2006-2009  Stephen Starkey

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/
package org.xptracker.util;

import java.io.Serializable;
import java.util.concurrent.*;

/**
 * A concurrent cache implementation which stores the results of 
 * invoking a defined {@link Callable} with different parameters.
 * Perhaps an updated version of EHCache coould do this...
 * @author caliendo
 *
 * @param <P> 
 *    the parameter type for the {@link Method}
 * @param <R>
 *    the return type of the {@link Method}
 */
public class ConcurrentMethodCache<P, R> implements Serializable {

  private final Method<P, R> method;
  private final ConcurrentMap<P, FutureTask<R>> cache = new ConcurrentHashMap<P, FutureTask<R>>(); 

  /**
   * Construct a new cache for the passed method.
   * @param method
   *    the method to cache
   */
  public ConcurrentMethodCache(final Method<P, R> method) {
    assert(method != null);

    this.method = method;
  }

  /**
   * Retrieve the result of the method call invoked with the passed 
   * parameter.  If the result has not been computed for the passed p
   * aramter yet, it will be computed, cached, and returned.
   * @param param 
   *    the parameter for the method invocation
   * @return
   *    the result of the method invocation
   * @throws InterruptedException 
   *    if the computation is interrupted
   * @throws ExecutionException 
   *    if the execution of the method resulted in an exception
   */
  public R get(final P param) throws InterruptedException, ExecutionException {
    FutureTask<R> result = this.cache.get(param);

    // if null, the result has not been computed for the parameter yet
    if (result == null) {
      result = new FutureTask<R>(new Callable<R>() {
        public R call() throws Exception {
          return method.invoke(param);
        }

      });
      final FutureTask<R> oldResult = this.cache.putIfAbsent(param, result);

      /*
       * if oldResult is null, that means another thread has not attempted to 
       * compute for this parameter yet since the last null check (so compute it).
       * this ensures that our method is only invoked once, since it will not be 
       * invoked by other threads while this FutureTask is executing.
       */
      if (oldResult == null) {
        result.run();
      } else {
        result = oldResult;
      }
    }

    return result.get();
  }

  /**
   * Remove the cached result for the method invocation with the passed parameter.
   * @param param
   *    the parameter to remove the cached result for
   */
  public synchronized void remove(final P param) {
    this.cache.remove(param);
  }

  /**
   * Clears all cached results.
   */
  public synchronized void clear() {
    this.cache.clear();
  }

  /**
   * Defines a method with a single parameter and return type.
   * @author caliendo
   *
   * @param <P> 
   *    the parameter type
   * @param <R>
   *    the return type
   */
  public static interface Method<P, R> extends Serializable {

    /**
     * Invoke the method with the passed parameter and returning 
     * a certain type.
     * @param param
     *    the parameter to pass to the invocation
     * @return
     *    an object of the specified return type
     * @throws Exception
     *    if an error occurs while executing the method
     */
    public R invoke(P param) throws Exception;
  }

}
